﻿/**
 * Peter
 * Created by: Peter Development Team
 *    http://peter.codeplex.com/
 * 
 * GNU General Public License version 2 (GPLv2)
 *    http://peter.codeplex.com/license
 *
 *  This code is provided on an AS IS basis, with no WARRANTIES,
 *  CONDITIONS or GUARANTEES of any kind.
 *
 **/

using System.ComponentModel;
using System.ComponentModel.Composition;
using System.IO;
using System.Text;
using System.Windows.Data;
using System.Windows.Input;
using System.Xml;
using System.Xml.Linq;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Folding;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Indentation.CSharp;
using System;
using System.Windows;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.NRefactory;
using Peter.Common;
using Peter.Common.Interfaces;
using Peter.Common.MainMenu;
using Peter.Common.Services;
using Peter.Controls;
using Peter.Data;
using Peter.Logic.AvalonEdit;
using Peter.Properties;

namespace Peter.Logic
{
   /// <summary>
   /// Represents the editor view model for peter.
   /// </summary>
   public sealed class ModelEditor : PeterDocument, IEditable, ICaret, IFileChangedNotification
   {
      #region Fields

      private const string NEW_DOC = "New Document";

      private string m_FindText;
      private string m_ReplaceText;
      private string m_GotoLineNumber;
      private string m_FindOccurences;
      private bool m_LocalSave;
      private bool m_IsReadOnly;
      private bool m_NeedsSaving;
      private bool m_FindCaseSensitive;
      private bool m_IsFindBarVisible;
      private bool m_IsGotoLineVisible;
      private RelayCommand m_CmdEdit;
      private IPeterOptions m_PeterOptions;
      private readonly ITimerService m_Timer;
      private IPeterFileWatcher m_FileWatcher;
      private FoldingManager m_FoldingManager;
      private IFoldingStrategy m_FoldingStrategy;
      private IHighlightingDefinition m_SyntaxHighlighting;
      private readonly FindTextMarkerService m_FindMarkerService;

      #endregion

      #region Setup

      /// <summary>
      /// Initializes a new editor model.
      /// </summary>
      public ModelEditor ()
      {
         this.Title = NEW_DOC;
         this.ToolTip = NEW_DOC;
         this.ContentId = Guids.EDITOR;
         this.Editor = new CodeEditor ();
         this.m_FindMarkerService = new FindTextMarkerService (this.Editor);
         this.Editor.TextChanged += this.OnTextChanged;
         this.Editor.TextArea.Caret.PositionChanged += this.OnCaretChanged;
         this.Editor.SetBinding (TextEditor.IsReadOnlyProperty, new Binding ("IsReadOnly"));
         this.Editor.SetBinding (TextEditor.IsModifiedProperty, new Binding ("NeedsSaving") { Mode = BindingMode.TwoWay });
         this.Editor.Focus ();

         this.m_Timer = ServiceManager.Get<ITimerService> ();
         this.m_Timer.Interval = 500;
         this.m_Timer.OnTimerTick += this.OnUpdateTimerTick;
      }

      #endregion

      #region Properties

      /// <summary>
      /// Gets the code editor.
      /// </summary>
      public CodeEditor Editor { get; private set; }

      /// <summary>
      /// Gets or Sets the peter search auto complete functionality.
      /// </summary>
      [Import (typeof (IPeterSearchAutoComplete))]
      public IPeterSearchAutoComplete SearchAutoComplete { get; set; }

      /// <summary>
      /// Gets or Sets the peter file watcher.
      /// </summary>
      [Import (typeof (IPeterFileWatcher))]
      public IPeterFileWatcher FileWatcher
      {
         get { return this.m_FileWatcher; }
         set
         {
            this.m_FileWatcher = value;
            if (this.FileInfo != null)
            {
               this.m_FileWatcher.AddWatch (this.FileInfo, this);
            }
         }
      }

      /// <summary>
      /// Gets the options.
      /// </summary>
      [Import (typeof (IPeterOptions))]
      public IPeterOptions Options
      {
         get { return this.m_PeterOptions; }
         set
         {
            this.m_PeterOptions = value;
            this.m_PeterOptions.EditorOptionsChanged += this.OnEditorOptionsChanged;
            if (!this.m_PeterOptions.IsLoaded)
               this.m_PeterOptions.Loaded += this.OnOptionsLoaded;
            else
            {
               var optionsEditor = (OptionsEditor) this.m_PeterOptions.GetOptionPage ("Editor");
               this.UpdateEditorOptions (optionsEditor);
            }
         }
      }

      /// <summary>
      /// Gets if the editable needs saving.
      /// </summary>
      public bool NeedsSaving
      {
         get { return this.m_NeedsSaving; }
         set
         {
            if (this.m_NeedsSaving != value)
            {
               this.m_NeedsSaving = value;
               this.RaisePropertyChanged ("NeedsSaving");
            }
         }
      }

      /// <summary>
      /// Gets the edit command.
      /// </summary>
      public ICommand EditCommand
      {
         get { return this.m_CmdEdit ?? (this.m_CmdEdit = new RelayCommand (this.OnEdit, this.CanEdit)); }
      }

      /// <summary>
      /// Checks whether an edit actions can be performed or not.
      /// </summary>
      /// <param name="obj">Edit action</param>
      /// <returns>True if action can be performed, otherwise false.</returns>
      private bool CanEdit (object obj)
      {
         var editAction = (EditAction) obj;
         switch (editAction)
         {
            case EditAction.Copy:
               return this.SelectionLength > 0;
            case EditAction.Cut:
               return this.SelectionLength > 0;
            case EditAction.Paste:
               return !this.IsReadOnly;
            case EditAction.SelectAll:
            case EditAction.Format:
            case EditAction.SaveAs:
               return true;
            case EditAction.Save:
               return this.NeedsSaving;
            case EditAction.Undo:
               return this.Editor.CanUndo;
            case EditAction.Redo:
               return this.Editor.CanRedo;
            default:
               return false;
         }
      }

      /// <summary>
      /// Performs the edit action.
      /// </summary>
      /// <param name="obj">Edit action.</param>
      private void OnEdit (object obj)
      {
         var editAction = (EditAction) obj;
         switch (editAction)
         {
            case EditAction.Copy:
               this.Editor.Copy ();
               break;
            case EditAction.Cut:
               this.Editor.Cut ();
               break;
            case EditAction.Paste:
               this.Editor.Paste ();
               break;
            case EditAction.SelectAll:
               this.Editor.SelectAll ();
               break;
            case EditAction.Format:
               this.Format ();
               break;
            case EditAction.Save:
               this.Save ();
               break;
            case EditAction.SaveAs:
               this.SaveAs ();
               break;
            case EditAction.Undo:
               this.Editor.Undo ();
               break;
            case EditAction.Redo:
               this.Editor.Redo ();
               break;
         }
      }

      /// <summary>
      /// Gets or Sets the text of the editor.
      /// </summary>
      public override string Text
      {
         get { return this.Editor.Document.Text; }
      }

      /// <summary>
      /// Gets or Sets if the file is read only or not.
      /// </summary>
      public bool IsReadOnly
      {
         get { return m_IsReadOnly; }
         set
         {
            this.m_IsReadOnly = value;
            this.RaisePropertyChanged ("IsReadOnly");
         }
      }

      /// <summary>
      /// Gets or Sets the syntax highlighting definition.
      /// </summary>
      public IHighlightingDefinition SyntaxHighlighting
      {
         get { return this.m_SyntaxHighlighting; }
         set
         {
            this.m_SyntaxHighlighting = value;
            this.RaisePropertyChanged ("SyntaxHighlighting");
         }
      }

      #endregion

      #region Editing

      /// <summary>
      /// Opens the current file.
      /// </summary>
      /// <param name="fileInfo">The file info object to open.</param>
      public void Open (FileInfo fileInfo)
      {
         if (fileInfo != null)
         {
            this.FileInfo = fileInfo;
            this.RaisePropertyChanged ("File");
            this.Title = this.FileInfo.Name;
            this.ToolTip = this.FileInfo.FullName;
            this.ContentId = string.Format ("{0}|{1}", Guids.EDITOR, this.FileInfo.FullName);
            if (this.FileInfo.Exists)
            {
               this.IsReadOnly = this.FileInfo.IsReadOnly;
               this.Editor.OpenFile (this.FileInfo);//.Load (this.FileInfo.FullName);
               this.Icon = MainMenuIconConverter.GetIconForFile (this.FileInfo);
               this.UpdateCodeEditor ();
            }

            if (this.FileWatcher != null)
            {
               this.FileWatcher.AddWatch (fileInfo, this);
            }
         }
         this.Editor.Focus ();
      }

      /// <summary>
      /// Saves the current document.
      /// </summary>
      private void Save ()
      {
         if (this.FileInfo == null)
            this.SaveAs ();
         else
         {
            this.m_LocalSave = true;
            this.Editor.SaveFile (this.FileInfo);
            this.UpdateCodeEditor ();
            this.Title = this.FileInfo.Name;
            this.ToolTip = this.FileInfo.FullName;
            PeterCommon.AddRecentFile (this.FileInfo);
         }
      }

      /// <summary>
      /// Saves the current document in a new file.
      /// </summary>
      private void SaveAs ()
      {
         var save = ServiceManager.Get<ISaveFileService> ();
         save.Title = "Peter - Save File";
         if (!save.ShowDialog ())
            return;
         if (this.FileWatcher != null && this.FileInfo != null)
            this.FileWatcher.RemoveWatch (this.FileInfo, this);
         this.FileInfo = new FileInfo (save.FileName);
         if (this.FileWatcher != null)
            this.FileWatcher.AddWatch (this.FileInfo, this);
         this.Save ();
      }

      /// <summary>
      /// Requests the editable to perform a format operation.
      /// </summary>
      private void Format ()
      {
         if (this.FileInfo != null)
         {
            if (CommonUtilities.IsXml (this.FileInfo.Extension))
            {
               try
               {
                  var stringBuilder = new StringBuilder ();
                  var element = XElement.Parse (this.Editor.Text);
                  var settings = new XmlWriterSettings
                  {
                     OmitXmlDeclaration = true,
                     Indent = true,
                     NewLineOnAttributes = true
                  };
                  using (var xmlWriter = XmlWriter.Create (stringBuilder, settings))
                  {
                     element.Save (xmlWriter);
                  }
                  this.Editor.Text = stringBuilder.ToString ();
               }
               catch (Exception ex)
               {
                  var status = ServiceManager.Get <IStatus> ();
                  if (status != null)
                  {
                     status.SetStatus (string.Format (Resources.BadXmlFormat, ex.Message), StatusTimeDelay.SuperLong,
                        StatusType.Error);
                  }
               }
            }
            else
            {
               var indentationStrategy = new CSharpIndentationStrategy ();
               var documentAssessor = new TextDocumentAccessor (this.Editor.Document);
               indentationStrategy.Indent (documentAssessor, true);
            }
         }
      }

      #endregion

      #region Document

      /// <summary>
      /// Scrolls to the given offset.
      /// </summary>
      /// <param name="offset">Offset to scroll to.</param>
      public override void ScrollToOffset (int offset)
      {
         if (offset <= this.Editor.Text.Length)
         {
            this.Editor.CaretOffset = offset;
            var line = this.Editor.Document.GetLineByOffset (offset);
            this.Editor.ScrollToLine (line.LineNumber);
         }
         else
         {
            this.Editor.CaretOffset = this.Editor.Text.Length;
            this.Editor.ScrollToEnd ();
         }
      }

      /// <summary>
      /// Scrolls the document to the given location.
      /// </summary>
      /// <param name="location">Location to scroll to.</param>
      public override void ScrollToLocation (TextLocation location)
      {
         this.Editor.CaretOffset = this.Editor.Document.GetOffset (location);
         this.Editor.ScrollTo (location.Line, location.Column);
         this.Peter.ActiveContent = this;
      }

      #endregion

      #region Text Changed

      /// <summary>
      /// Occurs when the text in the editor changes.
      /// </summary>
      /// <param name="sender">TextEditor</param>
      /// <param name="e">EventArgs</param>
      private void OnTextChanged (object sender, EventArgs e)
      {
         this.m_Timer.StopTimer ();
         this.RaiseTextChanged ();
         // Update find items...
         if (this.m_FindMarkerService != null && this.m_FindMarkerService.TextMarkerCount > 0)
            this.SetFindOccurrences (this.m_FindMarkerService.UpdateAfterOffset (this.Editor.CaretOffset));
         this.m_Timer.Start ();
      }

      /// <summary>
      /// Occurs when the update timer ticks, so we don't update so often.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void OnUpdateTimerTick (object sender, EventArgs e)
      {
         this.m_Timer.StopTimer ();
         this.UpdateCodeEditor ();
      }

      /// <summary>
      /// Updates attached items in the code editor.
      /// </summary>
      private void UpdateCodeEditor ()
      {
         // Update folding...
         if (this.m_FoldingStrategy != null)
            this.m_FoldingStrategy.UpdateFoldings (this.m_FoldingManager, this.Editor.Document);
         // Update find items...
         if (this.m_FindMarkerService != null && this.m_FindMarkerService.TextMarkerCount > 0)
            this.SetFindOccurrences (this.m_FindMarkerService.UpdateAfterOffset (this.Editor.CaretOffset));
      }

      #endregion

      #region Options

      /// <summary>
      /// Occurs when the options are loaded.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void OnOptionsLoaded (object sender, EventArgs e)
      {
         this.m_PeterOptions.Loaded -= this.OnOptionsLoaded;
         var optionsEditor = (OptionsEditor) this.m_PeterOptions.GetOptionPage ("Editor");
         this.UpdateEditorOptions (optionsEditor);
      }

      /// <summary>
      /// Updates all of the editor options.
      /// </summary>
      /// <param name="optionsEditor">The editor options to update to.</param>
      private void UpdateEditorOptions (OptionsEditor optionsEditor)
      {
         if (optionsEditor != null)
         {
            this.Editor.ShowLineNumbers = optionsEditor.ShowLineNumbers;
            this.Editor.WordWrap = optionsEditor.WrapText;
            this.Editor.Options.AllowScrollBelowDocument = optionsEditor.AllowScrollPastDocument;
            this.Editor.Options.ConvertTabsToSpaces = optionsEditor.ConvertTabsToSpaces;
            this.Editor.Options.EnableEmailHyperlinks = optionsEditor.EnableEmails;
            this.Editor.Options.EnableHyperlinks = optionsEditor.EnableHyperlinks;
            this.Editor.Options.IndentationSize = optionsEditor.IndentSize;
            this.Editor.Options.RequireControlModifierForHyperlinkClick = optionsEditor.HoldCtrlForHyperlinks;
            this.Editor.Options.ShowEndOfLine = optionsEditor.ShowEol;
            this.Editor.Options.ShowSpaces = optionsEditor.ShowSpaces;
            this.Editor.Options.ShowTabs = optionsEditor.ShowTabs;
            this.Editor.FontSize = optionsEditor.FontSize;
            this.Editor.FontFamily = optionsEditor.SelectedFont;
            this.Editor.SetLineHighlight(optionsEditor.HighlightCurrentLine, optionsEditor.HighlightLineColor,
               optionsEditor.HighlightLineBorderColor, optionsEditor.HighlightLineThickness);
            if (optionsEditor.CodeFolding && this.FileInfo != null)
            {
               if (this.m_FoldingManager != null)
                  FoldingManager.Uninstall (this.m_FoldingManager);
               this.m_FoldingManager = FoldingManager.Install (this.Editor.TextArea);
               if (CommonUtilities.IsXml (this.FileInfo.Extension))
                  this.m_FoldingStrategy = new XmlFoldingStrategy ();
               else
                  this.m_FoldingStrategy = new BraceFoldingStrategy ();
               this.m_FoldingStrategy.UpdateFoldings (this.m_FoldingManager, this.Editor.Document);
            }
            else
            {
               if (this.m_FoldingManager != null)
               {
                  FoldingManager.Uninstall (this.m_FoldingManager);
                  this.m_FoldingManager = null;
               }
            }
         }
      }

      /// <summary>
      /// Occurs when the editor options are changed.
      /// </summary>
      /// <param name="sender">Editor Options.</param>
      /// <param name="e">PropertyChangedEventArgs</param>
      private void OnEditorOptionsChanged (object sender, PropertyChangedEventArgs e)
      {
         this.UpdateEditorOptions ((OptionsEditor) sender);
      }

      #endregion

      #region Closing

      /// <summary>
      /// Closes the current item.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      protected override void OnClose (object obj)
      {
         if (this.ManageUnsavedContent ())
         {
            if (this.FileWatcher != null && this.FileInfo != null)
               this.FileWatcher.RemoveWatch (this.FileInfo, this);
            if (this.m_PeterOptions != null)
               this.m_PeterOptions.EditorOptionsChanged -= this.OnEditorOptionsChanged;
            this.Editor.TextChanged -= this.OnTextChanged;
            this.Editor.TextArea.Caret.PositionChanged -= this.OnCaretChanged;
            this.m_FindMarkerService.Remove ();
            this.Editor.Close ();
            this.m_Timer.StopTimer ();
            this.m_Timer.OnTimerTick = null;
            base.OnClose (obj);
         }
      }

      /// <summary>
      /// Manages the unsaved content while the user chooses to close it.
      /// </summary>
      public bool ManageUnsavedContent ()
      {
         var ok = true;
         if (this.NeedsSaving)
         {
            var messageBox = ServiceManager.Get<IMessageBoxService> ();
            var result = messageBox.ShowYesNoCancel (
                  String.Format ("File '{0}' {1}", this.FileInfo == null ? this.ToolTip
                  : this.FileInfo.FullName, Resources.SaveFileQuestion), Resources.SaveFileCaption);
            if (result == MessageBoxResult.Yes)
               this.Save ();
            ok = result != MessageBoxResult.Cancel;
         }
         return ok;
      }

      #endregion

      #region Find/Replace

      /// <summary>
      /// Gets the currently selected text.
      /// </summary>
      public string SelectedText
      {
         get { return this.Editor.SelectedText; }
      }

      /// <summary>
      /// Gets or Sets if the find bar is visible or not.
      /// </summary>
      public bool IsFindBarVisible
      {
         get { return this.m_IsFindBarVisible; }
         set
         {
            this.m_IsFindBarVisible = value;
            this.RaisePropertyChanged ("IsFindBarVisible");
         }
      }

      /// <summary>
      /// Gets or Sets if the find should ignore case or not.
      /// </summary>
      public bool FindCaseSensitive
      {
         get { return this.m_FindCaseSensitive; }
         set
         {
            this.m_FindCaseSensitive = value;
            this.RaisePropertyChanged ("FindCaseSensitive");
            this.SetFindOccurrences (this.m_FindMarkerService.MarkFindText (this.m_FindText, !this.FindCaseSensitive));
         }
      }

      /// <summary>
      /// Gets or Sets the Find Text.
      /// </summary>
      public string FindText
      {
         get { return m_FindText; }
         set
         {
            this.m_FindText = value;
            this.RaisePropertyChanged ("FindText");
            this.SetFindOccurrences (this.m_FindMarkerService.MarkFindText (this.m_FindText, !this.FindCaseSensitive));
         }
      }

      /// <summary>
      /// Gets or Sets the replace text.
      /// </summary>
      public string ReplaceText
      {
         get { return this.m_ReplaceText; }
         set
         {
            if (this.m_ReplaceText == value) return;
            this.m_ReplaceText = value;
            this.RaisePropertyChanged ("ReplaceText");
         }
      }

      /// <summary>
      /// Gets the number of matches.
      /// </summary>
      public string FindOccurrences
      {
         get { return this.m_FindOccurences; }
         private set
         {
            this.m_FindOccurences = value;
            this.RaisePropertyChanged ("FindOccurrences");
         }
      }

      /// <summary>
      /// Sets the text for the number of occurances found.
      /// </summary>
      /// <param name="matches">Number of matches found.</param>
      private void SetFindOccurrences (int matches)
      {
         if (matches == 0)
            this.FindOccurrences = "No matches found";
         else if (matches == 1)
            this.FindOccurrences = "1 match found";
         else
            this.FindOccurrences = string.Format ("{0} matches found", matches);
      }

      /// <summary>
      /// Occurs when the find bar is opening.
      /// </summary>
      public void FindBarOpening ()
      {
         if (this.SelectedText.Length > 0)
            this.FindText = this.SelectedText;
      }

      /// <summary>
      /// Occurs when the find bar is closing.
      /// </summary>
      public void FindBarClosing ()
      {
         this.SetFindOccurrences (0);
         if (this.m_FindMarkerService != null)
            this.m_FindMarkerService.RemoveAll ();
      }

      /// <summary>
      /// Searches for the next occurance of the given text.
      /// </summary>
      /// <returns>True if found, false if not found.</returns>
      public bool FindNext ()
      {
         return this.Find (SearchDirection.Next);
      }

      /// <summary>
      /// Searches for the previous occurance of the given text.
      /// </summary>
      /// <returns>True if found, false if not found.</returns>
      public bool FindPrevious ()
      {
         return this.Find (SearchDirection.Previous);
      }

      /// <summary>
      /// Searches for the previous occurance of the given text.
      /// </summary>
      /// <param name="direction">The search direction.</param>
      /// <returns>True if found, false if not found.</returns>
      public bool Find (SearchDirection direction)
      {
         bool found = false;
         var searchText = this.FindText;
         var textLength = searchText.Length;
         var caret = this.Editor.CaretOffset;
         var comparison = this.FindCaseSensitive
            ? StringComparison.Ordinal
            : StringComparison.OrdinalIgnoreCase;

         this.SearchAutoComplete.AddHistoryItem (searchText);
         if (direction == SearchDirection.Next)
         {
            var index = this.Editor.Text.IndexOf (searchText, caret, comparison);
            if (index >= 0)
            {
               found = true;
               this.SelectText (index, textLength);
            }

            if (!found)
            {
               index = this.Editor.Text.IndexOf (searchText, comparison);
               if (index >= 0)
               {
                  found = true;
                  this.SelectText (index, textLength);
                  StatusPassedEnd ();
               }
            }
         }
         else
         {
            var prevIndex = -1;
            var index = this.Editor.Text.IndexOf (searchText, comparison);
            while (index >= 0)
            {
               if (index < caret - textLength)
               {
                  prevIndex = index;
                  index = this.Editor.Text.IndexOf (searchText, index + 1, comparison);
               }
               else
               {
                  break;
               }
            }

            if (prevIndex == -1)
            {
               prevIndex = this.Editor.Text.LastIndexOf (searchText, comparison);
               if (prevIndex != -1)
                  StatusPassedEnd ();
            }

            if (prevIndex != -1)
            {
               found = true;
               this.SelectText (prevIndex, textLength);
            }
         }

         return found;
      }

      /// <summary>
      /// Replaces the next occurance of the find text with the replace text.
      /// </summary>
      public void Replace ()
      {
         if (string.IsNullOrWhiteSpace (this.Editor.SelectedText) || this.Editor.SelectedText != this.FindText)
            this.FindNext ();
         else
         {
            if (this.ReplaceText == null)
               this.ReplaceText = string.Empty;
            this.Editor.Document.Replace (this.SelectionStart, this.SelectionLength, this.ReplaceText);
         }
      }

      /// <summary>
      /// Replaces all occurances of the find text with the replace text.
      /// </summary>
      public void ReplaceAll ()
      {
         if (this.ReplaceText == null)
            this.ReplaceText = string.Empty;
         /*var count = Regex.Matches (this.Editor.Text, this.FindText,
               this.FindCaseSensitive ? RegexOptions.IgnoreCase : RegexOptions.None).Count;*/
         var count = 0;
         var index = 0;
         var textLength = this.FindText.Length;
         while (true)
         {
            index = this.Editor.Text.IndexOf (this.FindText, index, this.FindCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
            if (index != -1)
            {
               count++;
               this.Editor.Document.Replace (index, textLength, this.ReplaceText);
               //text = text.Substring (0, index) + this.ReplaceText + text.Substring (index + textLength);
               index += textLength;
            }
            else
               break;
         }
         if (count > 0)
         {
            /*var replaced = this.Editor.Text.Replace (this.FindText, this.ReplaceText);
            var replaced = Regex.Replace (this.Editor.Text, this.FindText, this.ReplaceText,
               this.FindCaseSensitive ? RegexOptions.IgnoreCase : RegexOptions.None);*/
            if (count == 1)
               SetStatus ("Replaced the only occurence of '" + this.FindText + "'.");
            else
               SetStatus ("Replaced '" + count + "' occurences of '" + this.FindText + "'.");
         }
         else
         {
            SetStatus ("No occurrences of '" + this.FindText + "' found.");
         }
         this.Editor.TextArea.Focus ();
      }

      /// <summary>
      /// Selects the text at the given index for the desired length.
      /// </summary>
      /// <param name="index">Index to select text at.</param>
      /// <param name="length">Length of text to select.</param>
      private void SelectText (int index, int length)
      {
         this.Editor.Select (index, length);
         var line = this.Editor.Document.GetLineByOffset (this.Editor.CaretOffset);
         var column = index - line.Offset;
         this.Editor.ScrollTo (line.LineNumber, column);
         this.Editor.TextArea.Focus ();
      }

      /// <summary>
      /// Sets the status bar text to the given message.
      /// </summary>
      /// <param name="statusMessage">Message to display on the status bar.</param>
      private static void SetStatus (string statusMessage)
      {
         var status = ServiceManager.Get<IStatus> ();
         if (status != null)
         {
            status.SetStatus (statusMessage, StatusTimeDelay.Medium, StatusType.Highlight);
         }
      }

      /// <summary>
      /// Sets the status bar text to passed the end of document.
      /// </summary>
      private static void StatusPassedEnd ()
      {
         var status = ServiceManager.Get<IStatus> ();
         if (status != null)
         {
            status.SetStatus (Resources.FindPassEnd, StatusTimeDelay.Short, StatusType.Highlight);
         }
      }

      #endregion

      #region Caret

      /// <summary>
      /// The line the caret is on.
      /// </summary>
      public int Line
      {
         get { return this.Editor.TextArea.Caret.Line; }
      }

      /// <summary>
      /// The character of the line the caret is on.
      /// </summary>
      public int Column
      {
         get { return this.Editor.TextArea.Caret.Column; }
      }

      /// <summary>
      /// The total offset of the caret.
      /// </summary>
      public int Offset
      {
         get { return this.Editor.TextArea.Caret.Offset; }
      }

      /// <summary>
      /// The length of the current selection.
      /// </summary>
      public int SelectionLength
      {
         get { return this.Editor.SelectionLength; }
      }

      /// <summary>
      /// The starting index of the selection.
      /// </summary>
      public int SelectionStart
      {
         get { return this.Editor.SelectionStart; }
      }

      /// <summary>
      /// Gets the text document.
      /// </summary>
      public TextDocument TextDocument
      {
         get { return this.Editor.Document; }
      }

      /// <summary>
      /// Occurs when the caret changes
      /// </summary>
      private void OnCaretChanged (object sender, EventArgs e)
      {
         this.RaisePropertyChanged ("Line");
         this.RaisePropertyChanged ("Column");
         this.RaisePropertyChanged ("Offset");
         this.Editor.TextArea.TextView.InvalidateLayer (KnownLayer.Selection);
      }

      #endregion

      #region IFileChangedNotification

      /// <summary>
      /// Invoked when a file has changed.
      /// </summary>
      /// <param name="fullPath">Full path to file.</param>
      public void FileChanged (string fullPath)
      {
         var lastModified = this.FileInfo.LastWriteTime;
         this.FileInfo.Refresh ();
         this.IsReadOnly = this.FileInfo.IsReadOnly;
         this.GuiDispatcher.Invoke (new Action (() => this.Editor.IsReadOnly = this.FileInfo.IsReadOnly), null);
         if (this.m_LocalSave)
         {
            this.m_LocalSave = false;
            return;
         }
         if (lastModified != this.FileInfo.LastWriteTime)
         {
            // TODO: Maybe ask if the user wants to reload...
            int selectionStart = 0;
            int selectionLength = 0;
            int caretLine = 0;
            int caretColumn = 0;
            this.GuiDispatcher.Invoke (new Action (() => selectionStart = this.Editor.SelectionStart), null);
            this.GuiDispatcher.Invoke (new Action (() => selectionLength = this.Editor.SelectionLength), null);
            this.GuiDispatcher.Invoke (new Action (() => caretLine = this.Editor.TextArea.Caret.Line), null);
            this.GuiDispatcher.Invoke (new Action (() => caretColumn = this.Editor.TextArea.Caret.Column), null);

            this.GuiDispatcher.Invoke (new Action (() => this.Editor.OpenFile (this.FileInfo)), null);
            this.GuiDispatcher.Invoke (new Action (() => this.Editor.Select (selectionStart, selectionLength)), null);
            this.GuiDispatcher.Invoke (new Action (() => this.Editor.ScrollTo (caretLine, caretColumn)), null);
         }
      }

      /// <summary>
      /// Invoked when a file has been created.
      /// </summary>
      /// <param name="fullPath">Full path to file.</param>
      public void FileCreated (string fullPath) { /* This will never happen... */ }

      /// <summary>
      /// Invoked when a file has deleted.
      /// </summary>
      /// <param name="fullPath">Full path to file.</param>
      public void FileDeleted (string fullPath)
      {
         // TODO: Ask user what they want to do...
      }

      /// <summary>
      /// Invoked when a file has changed.
      /// </summary>
      /// <param name="fullPath">Full path to file.</param>
      /// <param name="oldFullPath">The full path to the file before the rename.</param>
      public void FileRenamed (string fullPath, string oldFullPath)
      {
         // TODO: Ask user what they want to do...
      }

      #endregion

      #region Goto Line

      /// <summary>
      /// Gets or Sets if the goto line area is visible or not.
      /// </summary>
      public bool IsGotoLineVisible
      {
         get { return this.m_IsGotoLineVisible; }
         set
         {
            this.m_IsGotoLineVisible = value;
            this.RaisePropertyChanged ("IsGotoLineVisible");
         }
      }

      /// <summary>
      /// Gets or Sets the line number to go to.
      /// </summary>
      public string GotoLineNumber
      {
         get { return this.m_GotoLineNumber; }
         set
         {
            this.m_GotoLineNumber = value;
            this.RaisePropertyChanged ("GotoLineNumber");
         }
      }

      /// <summary>
      /// Scrolls the editor to the given line.
      /// </summary>
      public void GotoLine ()
      {
         int line;
         if (string.IsNullOrWhiteSpace (this.GotoLineNumber)) return;
         if (!Int32.TryParse (this.GotoLineNumber, out line)) return;
         this.Editor.ScrollToLine (line);
         this.Editor.CaretOffset = this.Editor.Document.GetOffset (line, 0);
         this.Editor.TextArea.Focus ();
         this.IsGotoLineVisible = false;
         this.GotoLineNumber = string.Empty;
      }

      #endregion
   }
}
